05. Numpy-数组的创建与访问

⭐ NumPy:Python科学计算的基石

  • NumPy(Numerical Python)是Python科学计算的基础包
  • 提供高性能的多维数组对象和相关数值计算工具
  • 是Pandas、Matplotlib、Scikit-learn等库的底层基础
  • 在金融数据分析中地位无可替代

⭐ NumPy数组 vs Python列表:核心差异

特性 Python列表 NumPy数组
内存存储 分散存储对象引用 连续内存存储
元素类型 可以不同 必须相同
性能 解释执行,较慢 编译优化,极快
向量化 不支持 原生支持
内存效率 较低 较高(紧凑存储)

⭐ 为什么NumPy更快?

  • 连续内存:缓存友好,减少内存访问延迟
  • 类型一致:避免动态类型检查的开销
  • 向量化操作:底层C/Fortran实现,利用SIMD指令
  • 广播机制:自动对齐不同形状的数组,无需手写循环

⭐ 平台任务代码

Listing 1
# ⚠️ 平台原始代码 - 请原样输入至教学平台(注释除外),平台才会判定答案正确
import numpy as np  # 导入NumPy数值计算库
# 已知股价数据
stock1=np.array([22.91,22.89,23.38,23.09,22.90])
stock2=np.array([41.14,40.99,41.29,40.81,40.97])  # 创建NumPy数组stock2
# 1. 查看股票1的属性值情况,并输出。
print(stock1.shape)
# 2. 输出股票2周二的股价
print(stock2[1])
# 3. 连接股票1与股票2的数据(按照2X5的方式连接),并输出
stock_con=np.concatenate(([stock1],[stock2]),axis=0)
print(stock_con)  # 输出两只股票收盘价合并后的2×5矩阵
(5,)
40.99
[[22.91 22.89 23.38 23.09 22.9 ]
 [41.14 40.99 41.29 40.81 40.97]]

⭐ np.array():从列表创建数组

Listing 2
import numpy as np

# 从Python列表创建一维NumPy数组
# 存储两只股票一周的收盘价
stock1 = np.array([22.91, 22.89, 23.38, 23.09, 22.90])
stock2 = np.array([41.14, 40.99, 41.29, 40.81, 40.97])

print(f'股票1收盘价: {stock1}')
print(f'股票2收盘价: {stock2}')
print(f'数据类型: {stock1.dtype}')  # float64:双精度浮点数
股票1收盘价: [22.91 22.89 23.38 23.09 22.9 ]
股票2收盘价: [41.14 40.99 41.29 40.81 40.97]
数据类型: float64

⭐ np.array()的关键参数

  • object:输入数据(列表、元组或其他数组)
  • dtype:指定数据类型(如np.float64np.int32
  • copy:是否复制数据(默认True)

金融数据类型选择

  • float64:双精度,金融计算推荐(价格、收益率)
  • float32:节省内存,适合大规模数据
  • int64:整数类型(成交量、日期索引)

⭐ 创建二维数组:多只股票数据

Listing 3
# 从嵌套列表创建二维数组
# 外层列表的每个元素是一行
stocks_2d = np.array([
    [22.91, 22.89, 23.38, 23.09, 22.90],  # 股票1的5日收盘价
    [41.14, 40.99, 41.29, 40.81, 40.97]   # 股票2的5日收盘价
])

print(f'二维数组形状: {stocks_2d.shape}')  # (2, 5):2行5列
print(f'行数: {stocks_2d.shape[0]}')       # 2
print(f'列数: {stocks_2d.shape[1]}')       # 5
二维数组形状: (2, 5)
行数: 2
列数: 5

⭐ 数组的关键属性

Listing 4
# .ndim:维度数(秩)
print(f'数组维度: {stocks_2d.ndim}')   # 2(二维数组)

# .size:元素总数
print(f'数组大小: {stocks_2d.size}')   # 10(2×5=10)

# .dtype:数据类型
print(f'数据类型: {stocks_2d.dtype}')  # float64
数组维度: 2
数组大小: 10
数据类型: float64

shape属性的维度含义

  • 一维 (5,):单只股票的时间序列
  • 二维 (2, 5):多只股票的时间序列
  • 三维 (2, 3, 5):多市场 × 多股票 × 多日(面板数据)

⭐ 一维数组索引:访问单个元素

Listing 5
# 索引从0开始:0=周一,1=周二,...,4=周五
tuesday_price = stock2[1]  # 获取周二的股价
print(f'股票2周二股价: {tuesday_price:.2f}元')  # 40.99元

# 负数索引从末尾开始
friday_price = stock2[-1]  # 最后一个元素(周五)
print(f'股票2周五股价: {friday_price:.2f}元')   # 40.97元
股票2周二股价: 40.99元
股票2周五股价: 40.97元

⭐ 一维数组切片:[start:end:step]

Listing 6
# 切片语法:[start:end:step]
# start包含,end不包含
print(f'前3天价格: {stock2[0:3]}')   # 索引0,1,2

# 省略start默认从0开始,省略end到末尾
print(f'后2天价格: {stock2[-2:]}')   # 倒数2个元素

# 步长为2,每隔一个取一个
print(f'隔日价格: {stock2[::2]}')    # 索引0,2,4
前3天价格: [41.14 40.99 41.29]
后2天价格: [40.81 40.97]
隔日价格: [41.14 41.29 40.97]

切片规则start包含,end不包含;负数索引从末尾计数

⭐ 布尔索引:条件筛选

Listing 7
# stock2 > 41.0 返回布尔数组
mask = stock2 > 41.0
print(f'布尔掩码: {mask}')

# 用布尔数组作为索引,返回True对应的元素
high_price_days = stock2[mask]
print(f'股价>41元的交易日: {high_price_days}')
布尔掩码: [ True False  True False False]
股价>41元的交易日: [41.14 41.29]
  • 比较运算符逐元素比较,返回布尔数组
  • 布尔数组作为索引,筛选满足条件的元素

⭐ 二维数组索引:array[row, col]

Listing 8
# 获取单个元素:array[行索引, 列索引]
element = stocks_2d[0, 2]  # 第1行第3列
print(f'stocks_2d[0, 2] = {element}')  # 23.38

# 获取整行:用 : 表示选择所有列
row_1 = stocks_2d[1, :]  # 第2行所有列
print(f'股票2所有交易日: {row_1}')

# 获取整列:用 : 表示选择所有行
col_3 = stocks_2d[:, 2]  # 所有行的第3列
print(f'周三所有股票价格: {col_3}')
stocks_2d[0, 2] = 23.38
股票2所有交易日: [41.14 40.99 41.29 40.81 40.97]
周三所有股票价格: [23.38 41.29]

⭐ 二维数组切片:获取子矩阵

Listing 9
# 所有行,第2-4列(索引1,2,3)
sub_matrix = stocks_2d[:, 1:4]
print(f'周二到周四的价格:\n{sub_matrix}')
周二到周四的价格:
[[22.89 23.38 23.09]
 [40.99 41.29 40.81]]

金融应用场景

  • 单只股票时间序列arr[stock_id, :]
  • 单日所有股票arr[:, day_id]
  • 特定时间段arr[:, start:end]

⭐ concatenate():数组拼接

Listing 10
# axis=0:沿行方向拼接(垂直堆叠)
stock_con = np.concatenate(([stock1], [stock2]), axis=0)
print(f'垂直拼接结果:\n{stock_con}')
print(f'形状: {stock_con.shape}')  # (2, 5)

# axis=0:一维数组首尾相接
prices_aug = np.array([22.95, 23.02])
stock2_aug = np.concatenate((stock2, prices_aug), axis=0)
print(f'\n扩充后的股票2: {stock2_aug}')
垂直拼接结果:
[[22.91 22.89 23.38 23.09 22.9 ]
 [41.14 40.99 41.29 40.81 40.97]]
形状: (2, 5)

扩充后的股票2: [41.14 40.99 41.29 40.81 40.97 22.95 23.02]

⭐ vstack与hstack:快捷拼接函数

Listing 11
# vstack:垂直堆叠(等价于concatenate axis=0)
stock_vstack = np.vstack((stock1, stock2))
print(f'垂直堆叠:\n{stock_vstack}')

# hstack:水平连接(等价于concatenate axis=1)
stock_hstack = np.hstack((stock1, stock2))
print(f'水平连接: {stock_hstack}')
垂直堆叠:
[[22.91 22.89 23.38 23.09 22.9 ]
 [41.14 40.99 41.29 40.81 40.97]]
水平连接: [22.91 22.89 23.38 23.09 22.9  41.14 40.99 41.29 40.81 40.97]
函数 功能 等价操作
concatenate 通用拼接(指定axis)
vstack 垂直堆叠 concatenate(..., axis=0)
hstack 水平连接 concatenate(..., axis=1)

⭐ 数组分割:hsplit与vsplit

Listing 12
# hsplit:水平分割(按列切分)
stocks_split = np.hsplit(stocks_2d, [2, 4])
print('按列分割结果:')
for i, part in enumerate(stocks_split):
    print(f'  部分{i+1}: 形状{part.shape}')

# vsplit:垂直分割(按行切分)
stocks_vsplit = np.vsplit(stocks_2d, 2)
print(f'\n按行分割:')
for i, part in enumerate(stocks_vsplit):
    print(f'  部分{i+1}: {part}')
按列分割结果:
  部分1: 形状(2, 2)
  部分2: 形状(2, 2)
  部分3: 形状(2, 1)

按行分割:
  部分1: [[22.91 22.89 23.38 23.09 22.9 ]]
  部分2: [[41.14 40.99 41.29 40.81 40.97]]

⭐ 视图(View)与副本(Copy)

  • 视图:共享原数组的内存,修改视图会影响原数组
  • 副本:拥有独立的内存,修改副本不影响原数组
  • 理解两者区别对避免意外数据修改至关重要

⭐ 切片返回视图,修改会影响原数组

Listing 13
original = np.array([1, 2, 3, 4, 5])

# 切片返回视图(共享内存)
view = original[1:4]
view[0] = 999  # 修改视图
print(f'修改视图后,原数组: {original}')  # [1, 999, 3, 4, 5]
修改视图后,原数组: [  1 999   3   4   5]
  • 切片操作 [1:4] 返回的是原数组的视图
  • 视图与原数组共享同一块内存

⭐ .copy()创建独立副本

Listing 14
original = np.array([1, 2, 3, 4, 5])

# .copy()创建副本(独立内存)
copy = original[1:4].copy()
copy[0] = 888  # 修改副本
print(f'修改副本后,原数组: {original}')  # [1, 2, 3, 4, 5]

# Fancy Indexing也返回副本
fancy = original[[0, 2, 4]]
fancy[0] = 777
print(f'Fancy索引修改后,原数组: {original}')  # [1, 2, 3, 4, 5]
修改副本后,原数组: [1 2 3 4 5]
Fancy索引修改后,原数组: [1 2 3 4 5]

⭐ 视图与副本规则总结

操作 返回类型 是否共享内存
切片 [start:end] 视图 ✅ 是
Fancy索引 [[0,2]] 副本 ❌ 否
布尔索引 [mask] 副本 ❌ 否
.copy() 副本 ❌ 否

最佳实践:不确定时,使用 .copy() 避免意外修改

⭐ reshape:改变数组形状

Listing 15
arr_1d = np.arange(10)  # 创建0-9的一维数组
print(f'一维数组: {arr_1d}')

# reshape(2, 5):转为2行5列,元素总数不变
arr_2x5 = arr_1d.reshape(2, 5)
print(f'\n2×5数组:\n{arr_2x5}')

# -1表示自动计算该维度
arr_5x2 = arr_1d.reshape(-1, 2)  # 自动计算行数=5
print(f'\n5×2数组:\n{arr_5x2}')
一维数组: [0 1 2 3 4 5 6 7 8 9]

2×5数组:
[[0 1 2 3 4]
 [5 6 7 8 9]]

5×2数组:
[[0 1]
 [2 3]
 [4 5]
 [6 7]
 [8 9]]

⭐ 展平与转置

Listing 16
arr_2x5 = np.arange(10).reshape(2, 5)

# ravel():展平为一维(可能返回视图)
flat_ravel = arr_2x5.ravel()
print(f'ravel结果: {flat_ravel}')

# flatten():展平为一维(总是返回副本)
flat_flatten = arr_2x5.flatten()
print(f'flatten结果: {flat_flatten}')

# .T:转置(行列互换)
transpose = arr_2x5.T  # 2×5 → 5×2
print(f'\n转置后 {transpose.shape}:\n{transpose}')
ravel结果: [0 1 2 3 4 5 6 7 8 9]
flatten结果: [0 1 2 3 4 5 6 7 8 9]

转置后 (5, 2):
[[0 5]
 [1 6]
 [2 7]
 [3 8]
 [4 9]]

⭐ 广播(Broadcasting)机制

广播允许不同形状的数组进行算术运算,NumPy会自动扩展较小的数组。

广播规则

  1. 尾部(最右边)开始比较维度
  2. 维度相同或其中一个为1则兼容
  3. 缺失的维度视为1

⭐ 广播示例:标量与数组

Listing 17
prices = np.array([10, 20, 30, 40, 50])
discount = 0.9  # 打9折

# 标量自动"广播"到数组的每个元素
discounted_prices = prices * discount
print(f'原价: {prices}')
print(f'折扣后: {discounted_prices}')
原价: [10 20 30 40 50]
折扣后: [ 9. 18. 27. 36. 45.]
  • 标量 0.9 被自动扩展为 [0.9, 0.9, 0.9, 0.9, 0.9]
  • 等价于逐元素相乘,但无需手写循环

⭐ 广播示例:不同形状数组运算

Listing 18
base_prices = np.array([10, 20, 30])        # 形状 (3,)
adjustments = np.array([[1.1], [0.9], [1.0]])  # 形状 (3, 1)

# 广播:(3,) → (3,3),(3,1) → (3,3)
result = base_prices * adjustments
print(f'调整后的价格矩阵:\n{result}')
调整后的价格矩阵:
[[11. 22. 33.]
 [ 9. 18. 27.]
 [10. 20. 30.]]

⭐ 广播应用:收益率标准化

Listing 19
returns = np.array([[0.05, 0.03, 0.04],
                    [0.06, 0.02, 0.05]])  # 2只股票,3个时间点

# keepdims=True 保持维度,便于广播
mean = returns.mean(axis=1, keepdims=True)  # (2,1)
std = returns.std(axis=1, keepdims=True)    # (2,1)

# 标准化:(收益率 - 均值) / 标准差
normalized = (returns - mean) / std
print(f'标准化后的收益率:\n{normalized}')
标准化后的收益率:
[[ 1.22474487 -1.22474487  0.        ]
 [ 0.98058068 -1.37281295  0.39223227]]

⭐ 向量化 vs 循环:性能对比

Listing 20
import time

n = 1000000  # 100万个数据点
prices = np.random.randn(n) * 10 + 100  # 模拟股价数据

# 方法1:Python循环(慢)
start = time.time()
returns_loop = []
for i in range(1, len(prices)):
    ret = (prices[i] - prices[i-1]) / prices[i-1]
    returns_loop.append(ret)
loop_time = time.time() - start

# 方法2:NumPy向量化(快)
start = time.time()
returns_vec = (prices[1:] - prices[:-1]) / prices[:-1]
vec_time = time.time() - start

print(f'循环时间: {loop_time:.4f}秒')
print(f'向量化时间: {vec_time:.4f}秒')
print(f'性能提升: {loop_time/vec_time:.1f}倍')
循环时间: 0.3623秒
向量化时间: 0.0034秒
性能提升: 105.3倍

⭐ 本章核心要点回顾

  • 创建数组np.array() 从列表创建,支持多维
  • 索引切片[start:end:step],二维用 [row, col]
  • 布尔索引:条件筛选数组元素
  • 拼接分割concatenate/vstack/hstackhsplit/vsplit
  • 视图与副本:切片返回视图,.copy() 创建独立副本
  • 广播机制:不同形状数组自动对齐运算